﻿// <copyright>
// Copyright (c) 2008 - 2009, Microsoft, All Rights Reserved
// </copyright>
//
// Licensed under the Microsoft Public License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at 
//
// http://www.opensource.org/licenses/ms-pl.html

namespace LiveLabs.Logging.Logs
{
    using System;
    using System.Globalization;
    using LiveLabs.Logging.Resources;
    using LiveLabs.Logging.Util;

    /// <summary>
    /// Implementation of <see cref="Log"/> that writes to a file.  The file will
    /// rollover on a timed schedule.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This class will take the file path and append a date to it based on the <see cref="TimedRollingSchedule"/>.
    /// If it is <see cref="TimedRollingSchedule.Hourly"/>, the extension .yyyy-MM-dd-HH is added to the file path.
    /// If it is <see cref="TimedRollingSchedule.Daily"/>, the extension .yyyy-MM-dd is added to the file path.
    /// All files are opened in the <see cref="System.IO.FileMode.Append"/> mode using
    /// <see cref="System.IO.FileAccess.Write"/> access.  They are shared to allow other processes to 
    /// <see cref="System.IO.FileShare.Read"/>.
    /// </para>
    /// <para>
    /// In both cases, the values for the components in the extension are controlled by
    /// the <see cref="TimedRollingTimeBasis"/>.
    /// </para>
    /// <para>
    /// This implementation is NOT thread safe.  See <see cref="LockingLog"/> to make an instance thread safe.
    /// </para>
    /// <para>
    /// You should be very careful about switching the <see cref="TimedRollingTimeBasis"/> in between 
    /// application executions.  By switching, you may go "forward" or "backward" in time depending on your 
    /// time zone.  This might cause existing files to be overwritten.
    /// </para>
    /// </remarks>
    public class TimedRollingLog : Log, IDisposable
    {
        private readonly string baseFilePath;

        private readonly TimedRollingSchedule schedule;

        private readonly TimedRollingTimeBasis timeBasis;

        private long nextRollover;

        private FileLog log;

        private volatile bool disposed;

        /// <summary>
        /// Initializes a new instance of the TimedRollingLog class.
        /// </summary>
        /// <param name="filePath">The relative or complete path to the file to log to.</param>
        /// <remarks>
        /// <para>
        /// This constructor initializes the rolling on an <see cref="TimedRollingSchedule.Hourly"/> basis
        /// using <see cref="TimedRollingTimeBasis.Utc"/> times.
        /// </para>
        /// </remarks>
        public TimedRollingLog(string filePath)
            : this(filePath, TimedRollingSchedule.Hourly, TimedRollingTimeBasis.Utc)
        {
        }

        /// <summary>
        /// Initializes a new instance of the TimedRollingLog class.
        /// </summary>
        /// <param name="filePath">The relative or complete path to the file to log to.</param>
        /// <param name="schedule">The schedule which the file should be rolled</param>
        /// <remarks>
        /// <para>
        /// This constructor initializes the rolling to <see cref="TimedRollingTimeBasis.Utc"/> times.
        /// </para>
        /// </remarks>
        public TimedRollingLog(string filePath, TimedRollingSchedule schedule)
            : this(filePath, schedule, TimedRollingTimeBasis.Utc)
        {
        }

        /// <summary>
        /// Initializes a new instance of the TimedRollingLog class.
        /// </summary>
        /// <param name="filePath">The relative or complete path to the file to log to.</param>
        /// <param name="timeBasis">The time basis which the file extensions should be based on.</param>
        /// <remarks>
        /// <para>
        /// This constructor initializes the rolling on an <see cref="TimedRollingSchedule.Hourly"/> schedule.
        /// </para>
        /// </remarks>
        public TimedRollingLog(string filePath, TimedRollingTimeBasis timeBasis)
            : this(filePath, TimedRollingSchedule.Hourly, timeBasis)
        {
        }

        /// <summary>
        /// Initializes a new instance of the TimedRollingLog class.
        /// </summary>
        /// <param name="filePath">The relative or complete path to the file to log to.</param>
        /// <param name="schedule">The schedule which the file should be rolled</param>
        /// <param name="timeBasis">The time basis which the file extensions should be based on.</param>
        public TimedRollingLog(string filePath, TimedRollingSchedule schedule, TimedRollingTimeBasis timeBasis)
        {
            if (string.IsNullOrEmpty(filePath))
            {
                throw new ArgumentNullException("filePath");
            }

            if (schedule != TimedRollingSchedule.Daily && schedule != TimedRollingSchedule.Hourly)
            {
                throw new ArgumentOutOfRangeException("schedule", schedule, Messages.TimedScheduleOutOfRange);
            }

            if (timeBasis != TimedRollingTimeBasis.Local && timeBasis != TimedRollingTimeBasis.Utc)
            {
                throw new ArgumentOutOfRangeException("timeBasis", timeBasis, Messages.TimedTimeBasisOutOfRange);
            }

            this.baseFilePath = filePath;
            this.schedule = schedule;
            this.timeBasis = timeBasis;
            this.disposed = false;

            this.CreateLogFile();
            this.CalculateNextRolloverTime();
        }

        private DateTime Now
        {
            get { return this.timeBasis == TimedRollingTimeBasis.Utc ? Clock.UtcNow : Clock.Now; }
        }

        /// <summary>
        /// Writes the message to a file.
        /// </summary>
        /// <param name="message">The message to write</param>
        /// <param name="level">The <see cref="LogLevel"/> of the message.  Ignore.</param>
        public override void WriteLine(string message, LogLevel level)
        {
            this.RollFileIfNecessary();

            this.log.WriteLine(message, level);
        }

        /// <summary>
        /// Disposes of any resources we may be using.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void CreateLogFile()
        {
            if (this.log != null)
            {
                this.log.Dispose();
                this.log = null;
            }

            this.log = new FileLog(this.GetLogFileName(), true);
        }

        private string GetLogFileName()
        {
            DateTime now = this.Now;
            string extension = string.Empty;
            switch (this.schedule)
            {
            case TimedRollingSchedule.Hourly:
                extension = now.ToString("yyyy-MM-dd-HH", CultureInfo.InvariantCulture);
                break;

            case TimedRollingSchedule.Daily:
                extension = now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
                break;
            }

            return this.baseFilePath + '.' + extension;
        }

        private void CalculateNextRolloverTime()
        {
            DateTime now = this.Now;

            switch (this.schedule)
            {
            case TimedRollingSchedule.Hourly:
                now += TimeSpan.FromHours(1);
                now = new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0, 0, now.Kind);
                break;

            case TimedRollingSchedule.Daily:
                now = (now + TimeSpan.FromDays(1)).Date;
                break;
            }

            this.nextRollover = now.Ticks;
        }

        private void RollFileIfNecessary()
        {
            if (this.Now.Ticks > this.nextRollover)
            {
                this.CreateLogFile();

                this.CalculateNextRolloverTime();
            }
        }

        private void Dispose(bool disposing)
        {
            if (this.disposed)
            {
                return;
            }

            try
            {
                if (disposing)
                {
                    this.log.Dispose();
                    this.log = null;
                }
            }
            finally
            {
                // we are disposed no matter what
                this.disposed = true;
            }
        }
    }
}
